1 认识帧动画
1.1 认识帧动画
什么是帧动画?
所谓帧动画就是在“连续的关键帧”中分解动画动作,在时间轴的每帧上逐帧绘制不同的内容,使其连续播放而成动画。
由于是一帧一帧地画,所以帧动画具有非常大的灵活性,几乎可以表现任何想表现的内容。
常见帧动画方式
- GIF
- CSS3 animation
- JavaScript
GIF 和 CSS3 animation 实现帧动画的不足
- (GIF 、 CSS3 animation)不能灵活地控制动画的
暂停
和播放
- (GIF)不能
捕捉
到动画完成的事件
- (GIF 、 CSS3 animation)不能对帧动画做更加灵活的扩展
JS 实现帧动画的原理
- 如果有多张帧图片,用一个 image 标签承载图片,
定时改变 image 的 src 属性
(不推荐) - 把所有动画关键帧
绘制在一张图片
里,把图片作为元素的 background-image,定时改变元素的 background-position 属性(推荐)
Demo: 第二种方式简单实现
demo.html1
2
3
4
5
6
7
8
9
10
11
12
<html lang="en">
<head>
<meta charset="UTF-8">
<title>动画 demo</title>
<link rel="stylesheet" href="demo.css">
</head>
<body>
<div id="rabbit"></div>
<script src="demo.js"></script>
</body>
</html>
demo.css1
2
3
4#rabbit {
width: 102px;
height: 80px;
}
demo.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30/**
* Created by tonyearth on 2016/12/10.
*/
var imgUrl = 'rabbit-big.png'
var positions = [
'0 -854',
'-174 -852',
'-349 -852',
'-524 -852',
'-698 -852',
'-873 -848'
]
var ele = document.getElementById('rabbit')
animation(ele, positions, imgUrl)
function animation(ele, positions, imgUrl) {
ele.style.backgroundImage = 'url( ' + imgUrl + ')'
ele.style.backgroundRepeat = 'no-repeat'
var index = 0
function run() {
var pos = positions[index].split(' ')
ele.style.backgroundPosition = pos[0] + 'px ' + pos[1] + 'px'
index++
if (index >= positions.length) {
index = 0
}
setTimeout(run, 80)
}
run()
}
1 | $ python -m SimpleHTTPServer 8080 # 启动一个 python 自带的简单的静态服务器 |
1.2 设计通用帧动画库
01 需求分析
- 支持图片
预加载
。 - 支持
两种
动画播放方式(img src、backgroundPosition),及自定义每帧动画。 - 支持单组动画控制
循环次数
(可支持无限次)。 - 支持一组动画完成,进行
下一组
动画。 - 支持每个动画完成后有
等待时间
。 - 支持动画
暂停
和继续
播放。 - 支持
动画完成后
执行回调函数。
02 编程接口
1 | loadImage(imglist) // 预加载图片 |
03 调用方式
- 支持链式调用
- 期望用
动词的方式
描述接口
1 | var animation = require('animation') |
04 代码设计
我们把 “图片与加载->动画执行->动画结束”等一系列操作看成一条
任务链(数组)
。
任务链有两种类型的任务:
a. 同步执行完毕的。
b. 异步定时执行的(通过计时器或者 rafrequestAnimationFrame
)记录当前任务链的索引。
- 每个任务执行完毕后,通过调用
next
方法,执行下一个任务,同时更新任务链索引值。
2 设计帧动画库
2.1 接口定义
1 |
|
2.2 图片预加载实现
封装图片预加载功能为独立的 cmd 模块。
- 图片信息数据格式检查
- 加载超时处理
- 判断全部加载成功技巧:& 操作
- 任务完成后清除资源
imageLoader.js
1 | 'use stract' |
2.3 图片预加载的应用
- 导入与图片预加载模块(cmd)
- 作为一个同步任务加入到任务链
关键代码片段(animation.js)
1 | var loadImage = require('./imageLoader') |
2.4 从入口函数开始
- 任务对象
- 同步任务和异步任务
- 任务链实现
关键代码(animation.js)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54/**
* 执行任务
* @private
*/
Animation.prototype._runTask = function () {
// 没有任务或任务没有开始,则什么也不做
if (!this.taskQueue || this.state !== STATE_START) {
return
}
// 任务链已经全部执行完毕,则什么也不做
if (this.index === this.taskQueue.length) {
this.dispose()
return
}
// 获得任务链上的当前任务
var task = this.taskQueue[this.index]
if (task.type === TASK_SYNC) {
this._syncTask(task)
} else {
this._asyncTask(task)
}
}
/**
* 同步任务
* @param task 执行的任务对象
* @private
*/
Animation.prototype._syncTask = function (task) {
var me = this
var next = function () {
// 切换到下一个任务
me._next()
}
var taskFn = task.taskFn
taskFn(next)
}
/**
* 异步任务
* @param task 执行的任务对象
* @private
*/
Animation.prototype._asyncTask = function (task) {
}
/**
* 切换到下一个任务
* @private
*/
Animation.prototype._next = function () {
this.next++
this._runTask()
}
2.5 timeline 的实现
说明: timeline 作为一个模块,用来实现平滑的帧动画。
- 使用 requestAnimationFrame API 避免丢帧
- 包装 requestAnimationFrame API,兼容低版本浏览器
- +new Date() 替代 Date.now() 性能更好
- 记录动画执行的时长,帧与帧之间的间隔,保证帧动画的频率
1 | /** |
2.6 剩余接口实现
- changePosition
- changeSrc
- then
- reStart
- repeat
- repeatForever
- wait
- pause
- dispose
3 webpack打包及帧动画库演示
3.1 webpack打包和 demo 编写
环境
(1) 全局安装 webpack1
2$ npm i webpack -g
$ npm i webpack-dev-server -g
(2) package.json1
2
3
4
5
6
7{
"name": "application-name",
"version": "0.0.1",
"devDependencies": {
"webpack": "^1.12.11"
}
}
(3) webpack.config.js1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* Created by tonyearth on 2016/12/11.
*/
module.exports = {
entry: {
animation: './src/animation.js'
},
output: {
path: __dirname + '/build',// ./build
filename: '[name].js',// animation.js
library: 'animation',// 在全局注册一个 window.animation 对象
libraryTarget: 'umd'// 兼容 amd、 cmd 或挂载到 window
}
}
简单的 DEMO
demo.html1
2
3
4
5
6
7
8
9
10
11
12
13
<html lang="en">
<head>
<meta charset="UTF-8">
<title>动画 demo</title>
<link rel="stylesheet" href="demo.css">
</head>
<body>
<div id="rabbit"></div>
<script src="../build/animation.js"></script>
<script src="demo.js"></script>
</body>
</html>
demo.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19/**
* Created by tonyearth on 2016/12/10.
*/
var imgUrl = 'rabbit-big.png'
var positions = [
'0 -854',
'-174 -852',
'-349 -852',
'-524 -852',
'-698 -852',
'-873 -848'
]
var ele = document.getElementById('rabbit')
var animation = window.animation
var repeatAnimation = animation() // 获取实例
.loadImage([imgUrl]) // 任务1:预加载图片
.changePosition(ele, positions, imgUrl) // 任务2: 帧动画
.repeatForever() // 任务3:无限重复上面的动画
repeatAnimation.start(80)
3.2 demo 的完整实现
demo.html1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html lang="en">
<head>
<meta charset="UTF-8">
<title>动画 demo</title>
<link rel="stylesheet" href="demo.css">
</head>
<body>
<div id="rabbit1" class="rabbit"></div>
<div id="rabbit2" class="rabbit"></div>
<div id="rabbit3" class="rabbit"></div>
<div id="rabbit4" class="rabbit"></div>
<script src="../build/animation.js"></script>
<script src="demo.js"></script>
</body>
</html>
demo.js1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111/**
* Created by tonyearth on 2016/12/10.
*/
function $(id) {
return document.getElementById(id)
}
$rabbit1 = $('rabbit1')
$rabbit2 = $('rabbit2')
$rabbit3 = $('rabbit3')
$rabbit4 = $('rabbit4')
var images = ['rabbit-big.png', 'rabbit-lose.png', 'rabbit-win.png']
var rightRunningMap = ['0 -854', '-174 -852', '-349 -852', '-524 -852', '-698 -852', '-873 -848']
var leftRunningMap = ["0 -373", "-175 -376", "-350 -377", "-524 -377", "-699 -377", "-873 -379"]
var rabbitWinMap = ["0 0", "-198 0", "-401 0", "-609 0", "-816 0", "0 -96", "-208 -97", "-415 -97", "-623 -97", "-831 -97", "0 -203", "-207 -203", "-415 -203", "-623 -203", "-831 -203", "0 -307", "-206 -307", "-414 -307", "-623 -307"]
var rabbitLoseMap = ["0 0", "-163 0", "-327 0", "-491 0", "-655 0", "-819 0", "0 -135", "-166 -135", "-333 -135", "-500 -135", "-668 -135", "-835 -135", "0 -262"]
/* 动画1 */
function repeat () {
var repeatAnimation = animation()
.loadImage(images) // 任务1:预加载图片
.changePosition($rabbit1, rightRunningMap, images[0]) // 任务2: 帧动画
.repeatForever() // 任务3:无限重复上面的动画
repeatAnimation.start(80)
}
repeat()
/* 动画2 */
function win() {
var winAnimation = animation()
.loadImage(images)
.changePosition($rabbit3, rabbitWinMap, images[2])
.repeat(3)
.then(function () {
console.log('win animation repeat 3 times and finished!')
})
winAnimation.start(200)
}
win()
/* 动画3 */
function lose() {
var loseAnimation = animation()
.loadImage(images)
.changePosition($rabbit2, rabbitLoseMap, images[1])
.wait(1000)
.repeat(1)
.then(function () {
console.log('lose animation repeat 1 times and finished')
})
loseAnimation.start(200)
}
lose()
/* 动画4 */
function run() {
var speed = 6
var initLeft = 0
var finalLeft = 400
var frameLength = 6
var frame = 4
var right = true
var interval = 50
var runAnimation = animation()
.loadImage(images)
.enterFrame(function (success, time) {
var radio = time / interval
var position
var left
if (right) {
position = rightRunningMap[frame].split(' ')
left = Math.min(initLeft + speed * radio, finalLeft)
if (left === finalLeft) {
right = false
frame = 4
success()
return
}
}
else {
position = leftRunningMap[frame].split(' ')
left = Math.max(initLeft, finalLeft - speed * radio)
if (left === initLeft) {
right = true
frame = 4
success()
return
}
}
$rabbit4.style.backgroundImage = 'url(' + images[0] + ')'
$rabbit4.style.backgroundPosition = position[0] + 'px ' + position[1] + 'px'
$rabbit4.style.left = left + 'px'
frame++
if (frame === frameLength) {
frame = 0
}
})
.repeat(2)
.wait(1000)
.changePosition($rabbit4, rabbitWinMap, images[2])
.then(function () {
console.log('finished!')
})
runAnimation.start(interval)
}
run()